Powershell scripts/Defender for SQL servers on machines status report/Get-SqlVMProtectionStatusReport.ps1 (256 lines of code) (raw):
<#
.SYNOPSIS
Retrieves registry values from all underlying VMs of your SQL Virtual Machines (in the scope of the current subscription)
for every SQL instance (under HKLM:\SOFTWARE\Microsoft\AzureDefender\SQL\) and exports the results to an Excel file.
.DESCRIPTION
For each SQL VM (as returned by Get-AzSqlVM), this script:
- Determines its underlying Virtual Machine.
- Invokes a run command (with -AsJob) on that VM. The remote script:
• Enumerates all instance names (subkeys) under HKLM:\SOFTWARE\Microsoft\AzureDefender\SQL\
• Retrieves the registry values "SqlQueryProtection_Status" and "SqlQueryProtection_Timestamp"
• Converts the .NET ticks timestamp into an ISO 8601 date/time
• Outputs a JSON array of objects (one per SQL instance)
- The local script waits for all jobs to complete, parses each job’s JSON output (using the Message property).
- Finally, the results are exported to an Excel file.
.NOTES
- Requires the Az modules (Az.Accounts, Az.Compute, Az.SqlVirtualMachine) and the ImportExcel module.
- Ensure you are connected to your Azure account (Connect-AzAccount).
#>
param(
[Parameter(Mandatory=$true)]
[string]$SubscriptionIdOrName
)
# ----------------------
# Connect to Azure if not already connected and set the subscription context
# ----------------------
if (-not $SubscriptionIdOrName -or [string]::IsNullOrWhiteSpace($SubscriptionIdOrName)) {
Write-Error "A valid subscription id or name must be provided."
exit
}
if (-not (Get-AzContext)) { Connect-AzAccount }
$subscription = Get-AzSubscription | Where-Object { $_.Id -eq $SubscriptionIdOrName -or $_.Name -eq $SubscriptionIdOrName }
if (-not $subscription) {
Write-Error "Subscription not found. Exiting."
exit
}
Write-Output "Processing subscription: $($subscription.Name) ($($subscription.Id))"
Set-AzContext -SubscriptionId $subscription.Id | Out-Null
# Import Excel for the output
Import-Module ImportExcel -ErrorAction Stop
# ----------------------
# 1. Define Remote Script
# ----------------------
$remoteScript = @'
$baseRegPath = "HKLM:\SOFTWARE\Microsoft\AzureDefender\SQL"
$results = @()
# Enumerate each subkey (instance) under the SQL key
try {
$instances = Get-ChildItem -Path $baseRegPath -ErrorAction SilentlyContinue
foreach ($instance in $instances) {
$instanceName = $instance.PSChildName
try {
# Attempt to retrieve the registry values for this instance.
$regValues = Get-ItemProperty -Path $instance.PSPath -Name "SqlQueryProtection_Status", "SqlQueryProtection_Timestamp" -ErrorAction Stop
# Convert the .NET ticks (100-nanosecond intervals since 0001-01-01) into an ISO 8601 timestamp.
$ticks = $regValues.SqlQueryProtection_Timestamp
$baseDate = [datetime]"0001-01-01T00:00:00Z"
$dt = $baseDate.AddTicks($ticks)
$iso = $dt.ToString("o")
# Build the output object for this instance.
$obj = [PSCustomObject]@{
InstanceName = $instanceName
ProtectionStatus = $regValues.SqlQueryProtection_Status
LastUpdate = $iso
}
$results += $obj
}
catch {
Write-Error "Failed to retrieve registry values for instance '$instanceName'. Error: $_"
}
}
}
catch {
Write-Error "Failed to enumerate SQL registry keys under $baseRegPath. Error: $_"
}
# Output the collected objects as JSON.
$results | ConvertTo-Json -Depth 4
'@
# ----------------------
# 2. Loop Through SQL VMs and Start Jobs
# ----------------------
$jobs = @()
$finalResults = @()
# Retrieve SQL Virtual Machines in this subscription.
$sqlVms = Get-AzSqlVM
if (-not $sqlVms) {
Write-Output "No SQL VMs found in subscription $($subscription.Name). Exiting."
exit
}
Write-Output "Found $($sqlVms.Count) SQL Virtual Machines. Initiating processing of SQL VM protection status checks..."
foreach ($sqlVm in $sqlVms) {
# Get the underlying Virtual Machine's resource id from either VirtualMachineId or VirtualMachineResourceId.
if ($sqlVm.VirtualMachineId) {
$underlyingVmResourceId = $sqlVm.VirtualMachineId
}
elseif ($sqlVm.VirtualMachineResourceId) {
$underlyingVmResourceId = $sqlVm.VirtualMachineResourceId
}
else {
Write-Warning "SQL VM '$($sqlVm.Name)' does not have an underlying Virtual Machine resource id. Skipping."
$obj = [PSCustomObject]@{
"SQL VM Name" = $sqlVm.Name
"Instance Name" = ""
"Protection Status" = ""
"Last Update" = ""
"SQL VM Resource ID" = ""
"Failure Reason" = "No underlying Virtual Machine resource id"
}
$finalResults += $obj
continue
}
# Parse the resource id to extract the resource group and VM name.
# Expected format: /subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.Compute/virtualMachines/{vmName}
$parts = $underlyingVmResourceId -split '/'
if ($parts.Count -lt 9) {
Write-Warning "Unexpected resource id format for SQL VM '$($sqlVm.Name)'. Skipping."
$obj = [PSCustomObject]@{
"SQL VM Name" = $sqlVm.Name
"Instance Name" = ""
"Protection Status" = ""
"Last Update" = ""
"SQL VM Resource ID" = $underlyingVmResourceId
"Failure Reason" = "Unexpected resource id format"
}
$finalResults += $obj
continue
}
$vmResourceGroup = $parts[4]
$vmName = $parts[8]
# Invoke the run command on the underlying VM as a job.
$job = Invoke-AzVMRunCommand -ResourceGroupName $vmResourceGroup `
-Name $vmName `
-CommandId 'RunPowerShellScript' `
-ScriptString $remoteScript `
-AsJob
# Attach extra metadata to the job for later aggregation.
$job | Add-Member -MemberType NoteProperty -Name "VmName" -Value $vmName -Force
$job | Add-Member -MemberType NoteProperty -Name "SqlVmName" -Value $sqlVm.Name -Force
$job | Add-Member -MemberType NoteProperty -Name "SQLVMResourceId" -Value $sqlVm.ResourceId -Force
$jobs += $job
}
# ----------------------
# 3. Process Job Outputs and Aggregate Results
# ----------------------
# Process each job’s output.
if ($jobs.Count -gt 0) {
Write-Output "Waiting for all run command jobs to complete..."
Wait-Job -Job $jobs
foreach ($job in $jobs) {
if ($job.State -eq 'Failed') {
$jobErrorMessage = $job.Error[0].Exception.Message
if ($jobErrorMessage -match "authorization to perform action") {
Write-Warning "Authorization failed for VM '$($job.VmName)'. Error: $jobErrorMessage"
}
elseif ($jobErrorMessage -match "requires the VM to be running") {
Write-Warning "The operation requires the VM '$($job.VmName)' to be running. Error: $jobErrorMessage."
}
else {
Write-Warning "Failed to retrieve protection status from machine '$($job.VmName)'. Error: $jobErrorMessage"
}
# Add the failed job details to the final results with empty fields for status and last update
$obj = [PSCustomObject]@{
"SQL VM Name" = $job.SqlVmName
"Instance Name" = ""
"Protection Status" = ""
"Last Update" = ""
"SQL VM Resource ID" = $job.SQLVMResourceId
"Failure Reason" = $jobErrorMessage
}
$finalResults += $obj
continue
}
try {
$jobOutput = Receive-Job -Job $job
$jsonOutput = $jobOutput.Value[0].Message
# Check if the job output message is empty or white space.
if ([string]::IsNullOrWhiteSpace($jsonOutput)) {
Write-Warning "Protection status could not be retrieved from SQL VM '$($job.SqlVmName)'."
$obj = [PSCustomObject]@{
"SQL VM Name" = $job.SqlVmName
"Instance Name" = ""
"Protection Status" = ""
"Last Update" = ""
"SQL VM Resource ID" = $job.SQLVMResourceId
"Failure Reason" = "No protection status information was found on the machine."
}
$finalResults += $obj
continue
}
try {
$parsed = $jsonOutput | ConvertFrom-Json
}
catch {
Write-Warning "Failed to parse JSON output for SQL VM '$($job.SqlVmName)'. Raw output: $jsonOutput"
continue
}
# Ensure the parsed output is an array.
if ($parsed -isnot [System.Collections.IEnumerable]) {
$parsed = @($parsed)
}
# Check if the parsed array is empty.
if (-not $parsed -or $parsed.Count -eq 0) {
Write-Warning "Protection status could not be retrieved from SQL VM '$($job.SqlVmName)'."
$obj = [PSCustomObject]@{
"SQL VM Name" = $job.SqlVmName
"Instance Name" = ""
"Protection Status" = ""
"Last Update" = ""
"SQL VM Resource ID" = $job.SQLVMResourceId
"Failure Reason" = "No protection status information was found on the machine."
}
$finalResults += $obj
continue
}
foreach ($item in $parsed) {
$obj = [PSCustomObject]@{
"SQL VM Name" = $job.SqlVmName
"Instance Name" = $item.InstanceName
"Protection Status" = $item.ProtectionStatus
"Last Update" = $item.LastUpdate.ToString("o")
"SQL VM Resource ID" = $job.SQLVMResourceId
"Failure Reason" = ""
}
$finalResults += $obj
}
}
catch {
Write-Warning "Failed to retrieve protection status for SQL VM '$($job.SqlVmName)'. Error details: $_."
Write-Warning "Please verify that the VM is running, accessible, that the SQL IaaS Extension and Defender for SQL provisioning is successful."
$obj = [PSCustomObject]@{
"SQL VM Name" = $job.SqlVmName
"Instance Name" = ""
"Protection Status" = ""
"Last Update" = ""
"SQL VM Resource ID" = $job.SQLVMResourceId
"Failure Reason" = "No protection status information was found on the machine."
}
$finalResults += $obj
}
}
}
# ----------------------
# 4. Export Results to Excel with Subscription ID in the filename and versioning if needed
# ----------------------
$baseName = "SqlVmProtectionResults_$($subscription.Id)"
$excelFile = "$baseName.xlsx"
$version = 1
while (Test-Path $excelFile) {
$excelFile = "${baseName}($version).xlsx"
$version++
}
$finalResults | Export-Excel -Path $excelFile -AutoSize -WorksheetName "SQLVMs"
$failedCount = ($finalResults | Where-Object { $_."Failure Reason" -and $_."Failure Reason".Trim() -ne "" }).Count
$successCount = $finalResults.Count - $failedCount
$totalCount = $finalResults.Count
Write-Output "Out of $totalCount total instances found, successfully retrieved protection status for $successCount, and failed for $failedCount."
Write-Output "Export complete. Results saved to $excelFile."